package truerefactor.refactor;

import java.util.ArrayList;
import java.util.List;

import truerefactor.graph.Accessibility;
import truerefactor.graph.AttributeNode;
import truerefactor.graph.ClassNode;
import truerefactor.graph.CodeGraph;
import truerefactor.graph.CodeNode;
import truerefactor.graph.Edge;
import truerefactor.graph.EdgeType;
import truerefactor.graph.MethodNode;
import truerefactor.graph.TypePair;

/**
 * MoveField - An implementation of the Move Field refactoring technique
 * 
 * @author Isaac
 */
public class MoveField extends Refactoring {

    /**
     * Creates a new instance of the move field refactoring technique
     */
    public MoveField()
    {
        this.name = "Move Field";
        this.description = "";
    }

    /*
     * (non-Javadoc)
     * @see
     * truerefactor.refactor.Refactoring#execute(truerefactor.graph.CodeNode [])
     */
    @Override
    public void execute(CodeGraph graph)
    {
        if (operands.size() < 2)
            return;

        AttributeNode field = (AttributeNode) operands.get(0);
        ClassNode source = field.getParent();
        ClassNode target = (ClassNode) operands.get(1);

        if (source == null || target == null || field == null)
            return;

        if (source.equals(target))
            return;

        encapsulateField(graph, field);

        createField(graph, field, target);

        selectReferenceStrategy(graph, source, target);

        // 4. Remove the field on the source class
        if (graph.isEdge(source, field, EdgeType.Attribute))
        {
            graph.remove(graph.getEdge(source, field, EdgeType.Attribute));
        }

    }

    private void selectReferenceStrategy(CodeGraph graph, ClassNode source, ClassNode target)
    {
        // 3. Determine how to reference the target object from the source
        // a. An existing field or method may give you the target. If not, see
        // whether you can easily create a method that will do so. Failing that,
        // you will need to create a new field in the source that can store the
        // target. This may be a permanent change, but you can also do it
        // temporarily until you have refactored enough to remove it.

        if (!source.getMethodsByReturnType(target.getIdentifier()).isEmpty())
        {
            // TODO modify the code, will be implemented when code
            // generation is
            // implemented
        }
        else if (source.getAttributeByType(target.getIdentifier()) != null)
        {
            // TODO modify the code, will be implemented when code
            // generation is
            // implemented
        }
        else
        {
            AttributeNode aNode = new AttributeNode(source, "ref" + target.getIdentifier(), "");
            aNode.setAccessibility(Accessibility.Private);
            aNode.setType(target.getIdentifier());
            aNode.setAssociatedClass(target);
            aNode.setParent(source);
            source.addAttribute(aNode);

            graph.insert(aNode);
            graph.insert(new Edge(source, aNode, EdgeType.Attribute));
        }
    }

    private void encapsulateField(CodeGraph graph, AttributeNode field)
    {
        // 1. IF the field is public, use Encapsulate Field
        // a. If you are likely to be moving the methods that access it
        // frequently or if a lot of methods access the field, you may find it
        // useful to use Self-Encapsulate Field
        if (field.getAccessibility().equals(Accessibility.Public) && field.getAssociatedMethods().size() <= 3)
        {
            EncapsulateField encapsulate = new EncapsulateField();
            List<CodeNode> encapsulateOperands = new ArrayList<CodeNode>();
            encapsulateOperands.add(field);

            encapsulate.setOperands(encapsulateOperands);
            encapsulate.execute(graph);
        }
        else if (field.getAccessibility().equals(Accessibility.Public) && field.getAssociatedMethods().size() > 3)
        {
            SelfEncapsulateField selfEncapsulate = new SelfEncapsulateField();
            List<CodeNode> encapsulateOperands = new ArrayList<CodeNode>();
            encapsulateOperands.add(field);

            selfEncapsulate.setOperands(encapsulateOperands);
            selfEncapsulate.execute(graph);
        }
    }

    private void createField(CodeGraph graph, AttributeNode field, ClassNode target)
    {
        // 2. Create a field in the target class with getting and setting
        // methods.
        MethodNode getter = new MethodNode("get" + field.getIdentifier(), "", Accessibility.Public);
        MethodNode setter = new MethodNode("set" + field.getIdentifier(), "", Accessibility.Public);
        getter.setReturnType(field.getType());
        setter.setReturnType("void");

        setter.addParameter(field.getIdentifier(), new TypePair(field.getType(), field.isPrimitive()));

        boolean hasGetter = false;
        boolean hasSetter = false;

        ClassNode parent = field.getParent();
        for (CodeNode node : parent.getAttributes())
        {
            if (node.getIdentifier().equals(getter.getIdentifier()))
            {
                hasGetter = true;
            }

            if (node.getIdentifier().equals(setter.getIdentifier()))
            {
                hasSetter = true;
            }
        }

        graph.insert(new Edge(target, field, EdgeType.Attribute));
        target.addAttribute(field);

        if (!hasGetter)
        {
            graph.insert(getter);
            getter.setParentClass(target);
            graph.insert(new Edge(target, getter, EdgeType.Method));
            getter.addAssociatedAttribute(field);
            target.addMethod(getter);
        }
        if (!hasSetter)
        {
            graph.insert(setter);
            setter.setParentClass(target);
            graph.insert(new Edge(target, setter, EdgeType.Method));
            setter.addAssociatedAttribute(field);
            field.setParent(target);
            target.addAttribute(field);
            target.addMethod(setter);
        }

        if (field.isPrimitive())
        {
            if (!graph.isEdge(target, field.getAssociatedClass(), EdgeType.Association))
            {
                graph.insert(new Edge(target, field.getAssociatedClass(), EdgeType.Association));
                target.addAssociatedClass(graph.getClass(field.getType()));
            }
        }

        replaceReferences(field, getter, setter);
    }

    private void replaceReferences(AttributeNode field, MethodNode getter, MethodNode setter)
    {
        // 5. Replace all references to the source field with references to the
        // appropriate method on the target.
        // a. For accesses to the variable, replace the reference with a call to
        // the target object's getting method; for assignments, replace the
        // reference with a call to the setting method.
        // b. If the field is not private, look in all the subclasses of the
        // source for references.
        List<MethodNode> assocMethods = field.getAssociatedMethods();
        for (int i = 0; i < assocMethods.size(); i++)
        {
            MethodNode assoc = assocMethods.get(i);
            double val = Math.random();
            if (val < .5)
            {
                assoc.addOtherMethod(getter);
            }
            else
            {
                assoc.addOtherMethod(setter);
            }

            assoc.removeAssociatedAttribute(field);
        }

        for (MethodNode other : field.getOtherMethods())
        {
            double val = Math.random();
            if (val < .5)
            {
                other.addOtherMethod(getter);
            }
            else
            {
                other.addOtherMethod(setter);
            }

            other.removeAssociatedAttribute(field);
        }

        field.getOtherMethods().clear();
        field.getAssociatedMethods().clear();

        field.addAssociatedClassMethod(setter);
        field.addAssociatedClassMethod(getter);
    }
}
